package com.qozix.tileview;

import android.content.Context;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.view.View;

import com.qozix.layouts.AnchorLayout;
import com.qozix.layouts.ZoomPanLayout;
import com.qozix.tileview.detail.DetailLevelEventListener;
import com.qozix.tileview.detail.DetailLevelPatternParser;
import com.qozix.tileview.detail.DetailManager;
import com.qozix.tileview.geom.PositionManager;
import com.qozix.tileview.graphics.BitmapDecoder;
import com.qozix.tileview.graphics.BitmapDecoderHttp;
import com.qozix.tileview.hotspots.HotSpot;
import com.qozix.tileview.hotspots.HotSpotEventListener;
import com.qozix.tileview.hotspots.HotSpotManager;
import com.qozix.tileview.markers.CalloutManager;
import com.qozix.tileview.markers.MarkerEventListener;
import com.qozix.tileview.markers.MarkerManager;
import com.qozix.tileview.paths.DrawablePath;
import com.qozix.tileview.paths.PathHelper;
import com.qozix.tileview.paths.PathManager;
import com.qozix.tileview.samples.SampleManager;
import com.qozix.tileview.tiles.TileManager;
import com.qozix.tileview.tiles.TileRenderListener;
import com.qozix.tileview.tiles.selector.TileSetSelector;
import com.qozix.tileview.tiles.selector.TileSetSelectorMinimalUpScale;

import java.util.HashSet;
import java.util.List;

/**
 * The TileView widget is a subclass of ViewGroup that provides a mechanism to asynchronously display tile-based images,
 * with additional functionality for 2D dragging, flinging, pinch or double-tap to zoom, adding overlaying Views (markers),
 * built-in Hot Spot support, dynamic path drawing, multiple levels of detail, and support for any relative positioning or
 * coordinate system.
 * <p/>
 * <p>A minimal implementation:</p>
 * <p/>
 * <pre>{@code
 * TileView tileView = new TileView(this);
 * tileView.setSize(3000,5000);
 * tileView.addDetailLevel(1.0f, "path/to/tiles/%col%-%row%.jpg");
 * }</pre>
 * <p/>
 * A more advanced implementation might look like:
 * <pre>{@code
 * TileView tileView = new TileView(this);
 * tileView.setSize(3000,5000);
 * tileView.addTileViewEventListener(someMapEventListener);
 * tileView.defineRelativeBounds(42.379676, -71.094919, 42.346550, -71.040280);
 * tileView.addDetailLevel(1.000f, "tiles/boston-1000-%col%_%row%.jpg", 256, 256);
 * tileView.addDetailLevel(0.500f, "tiles/boston-500-%col%_%row%.jpg", 256, 256);
 * tileView.addDetailLevel(0.250f, "tiles/boston-250-%col%_%row%.jpg", 256, 256);
 * tileView.addDetailLevel(0.125f, "tiles/boston-125-%col%_%row%.jpg", 128, 128);
 * tileView.addMarker(someView, 42.35848, -71.063736);
 * tileView.addMarker(anotherView, 42.3665, -71.05224);
 * tileView.addMarkerEventListener(someMarkerEventListener);
 * }</pre>
 */
public class TileView extends ZoomPanLayout {

    private HashSet<TileViewEventListener> tileViewEventListeners = new HashSet<TileViewEventListener>();

    private DetailManager detailManager = new DetailManager();
    private PositionManager positionManager = new PositionManager();

    private HotSpotManager hotSpotManager = new HotSpotManager(detailManager);

    private SampleManager sampleManager;
    private TileManager tileManager;
    private PathManager pathManager;
    private MarkerManager markerManager;
    private CalloutManager calloutManager;


    /**
     * Constructor to use when creating a TileView from code.  Inflating from XML is not currently supported.
     *
     * @param context (Context) The Context the TileView is running in, through which it can access the current theme, resources, etc.
     */
    public TileView(Context context) {

        super(context);

        sampleManager = new SampleManager(context, detailManager);
        addView(sampleManager);

        tileManager = new TileManager(context, detailManager);
        addView(tileManager);

        pathManager = new PathManager(context, detailManager);
        addView(pathManager);

        markerManager = new MarkerManager(context, detailManager);
        addView(markerManager);

        calloutManager = new CalloutManager(context, detailManager);
        addView(calloutManager);

        detailManager.addDetailLevelEventListener(detailLevelEventListener);
        tileManager.setTileRenderListener(renderListener);

        addZoomPanListener(zoomPanListener);
        addGestureListener(gestureListener);

        requestRender();

    }

    //------------------------------------------------------------------------------------
    // PUBLIC API
    //------------------------------------------------------------------------------------

    //------------------------------------------------------------------------------------
    // Event Management API
    //------------------------------------------------------------------------------------

    /**
     * Register an event listener callback object for this TileView.
     * Note this is method adds a listener to an array of listeners, and does not set
     * a single listener member a single listener.
     *
     * @param listener (TileViewEventListener) an implementation of the TileViewEventListener interface
     */
    public void addTileViewEventListener(TileViewEventListener listener) {
        tileViewEventListeners.add(listener);
    }

    /**
     * Removes a TileViewEventListener object from those listening to this TileView.
     *
     * @param listener (TileViewEventListener) an implementation of the TileViewEventListener interface
     */
    public void removeTileViewEventListener(TileViewEventListener listener) {
        tileViewEventListeners.remove(listener);
    }

    //------------------------------------------------------------------------------------
    // Rendering API
    //------------------------------------------------------------------------------------

    /**
     * Request that the current tile set is re-examined and re-drawn.
     * The request is added to a queue and is not guaranteed to be processed at any particular
     * time, and will never be handled immediately.
     */
    public void requestRender() {
        tileManager.requestRender();
    }

    /**
     * Notify the TileView that it may stop rendering tiles.  The rendering thread will be
     * sent an interrupt request, but no guarantee is provided when the request will be responded to.
     */
    public void cancelRender() {
        tileManager.cancelRender();
    }

    /**
     * Enables or disables tile image caching (in-memory and on-disk)
     *
     * @param shouldCache (boolean) true to enable caching, false to disable it (default)
     */
    public void setCacheEnabled(boolean shouldCache) {
        tileManager.setCacheEnabled(shouldCache);
    }

    /**
     * Sets a custom class to perform the decode operation when tile bitmaps are requested.
     * By default, a BitmapDecoder implementation is provided that renders bitmaps from the context's Assets,
     * but alternative implementations could be used that fetch images via HTTP, or from the SD card, or resources, SVG, etc.
     * This signature is identical to calling setTileDecoder and setDownsampleDecoder with the same decoder instance as the parameter.
     * {@link BitmapDecoderHttp} is an example of such an implementation.
     *
     * @param decoder (BitmapDecoder) A class instance that implements BitmapDecoder, and must define a decode method, which accepts a String file name and a Context object, and returns a Bitmap
     */
    public void setDecoder(BitmapDecoder decoder) {
        setTileDecoder(decoder);
        setDownsampleDecoder(decoder);
    }

    /**
     * Sets a custom class to perform the decode operation when tile bitmaps are requested for tile images only.
     * By default, a BitmapDecoder implementation is provided that renders bitmaps from the context's Assets,
     * but alternative implementations could be used that fetch images via HTTP, or from the SD card, or resources, SVG, etc.
     * {@link BitmapDecoderHttp} is an example of such an implementation.
     *
     * @param decoder (BitmapDecoder) A class instance that implements BitmapDecoder, and must define a decode method, which accepts a String file name and a Context object, and returns a Bitmap
     */
    public void setTileDecoder(BitmapDecoder decoder) {
        tileManager.setDecoder(decoder);
    }

    /**
     * Sets a custom class to perform the decode operation when tile bitmaps are requested for downsample images only.
     * By default, a BitmapDecoder implementation is provided that renders bitmaps from the context's Assets,
     * but alternative implementations could be used that fetch images via HTTP, or from the SD card, or resources, SVG, etc.
     * {@link BitmapDecoderHttp} is an example of such an implementation.
     *
     * @param decoder (BitmapDecoder) A class instance that implements BitmapDecoder, and must define a decode method, which accepts a String file name and a Context object, and returns a Bitmap
     */
    public void setDownsampleDecoder(BitmapDecoder decoder) {
        sampleManager.setDecoder(decoder);
    }

    /**
     * Get the {@link TileSetSelector} implementation currently used to select tile sets.
     *
     * @return TileSetSelector implementation currently in use.
     */
    public TileSetSelector getTileSetSelector() {
        return detailManager.getTileSetSelector();
    }

    /**
     * Set the tile selection method, defaults to {@link TileSetSelectorMinimalUpScale}
     * Implement the {@link TileSetSelector} interface to customize how tile sets are selected.
     *
     * @param selector (TileSetSelector) implementation that handles tile set selection as scale is changed.
     */
    public void setTileSetSelector(TileSetSelector selector) {
        detailManager.setTileSetSelector(selector);
    }

    /**
     * Defines whether tile bitmaps should be rendered using an AlphaAnimation
     *
     * @param enabled (boolean) true if the TileView should render tiles with fade transitions
     */
    public void setTransitionsEnabled(boolean enabled) {
        tileManager.setTransitionsEnabled(enabled);
    }

    /**
     * Define the duration (in milliseconds) for each tile transition.
     *
     * @param duration (int) the duration of the transition in milliseconds.
     */
    public void setTransitionDuration(int duration) {
        tileManager.setTransitionDuration(duration);
    }

    //------------------------------------------------------------------------------------
    // Detail Level Management API
    //------------------------------------------------------------------------------------

    /**
     * Defines the total size, in pixels, of the tile set at 100% scale.
     * The TileView wills pan within it's layout dimensions, with the content (scrollable)
     * size defined by this method.
     *
     */
    @Override
    public void setSize(int w, int h) {
        // super (define clip area)
        super.setSize(w, h);
        // coordinate with other components
        detailManager.setSize(w, h);
        // notify manager for relative positioning
        positionManager.setSize(w, h);
    }

    /**
     * Register a tile set to be used for a particular detail level.
     * Each tile set to be used must be registered using this method,
     * and at least one tile set must be registered for the TileView to render any tiles.
     *
     * @param detailScale (float) scale at which the TileView should use the tiles in this set.
     * @param pattern     (String) string path to the location of the tile image files, to be parsed by a DetailLevelPatternParser
     */
    public void addDetailLevel(float detailScale, String pattern) {
        detailManager.addDetailLevel(detailScale, pattern, null);
    }

    /**
     * Register a tile set to be used for a particular detail level.
     * Each tile set to be used must be registered using this method,
     * and at least one tile set must be registered for the TileView to render any tiles.
     *
     * @param detailScale (float) scale at which the TileView should use the tiles in this set.
     * @param pattern     (String) string path to the location of the tile image files, to be parsed by a DetailLevelPatternParser
     * @param downsample  (String) string path to the location of an optional non-tiled single image file that will fill the tile view, on a z-layer below tiles
     */
    public void addDetailLevel(float detailScale, String pattern, String downsample) {
        detailManager.addDetailLevel(detailScale, pattern, downsample);
    }

    /**
     * Register a tile set to be used for a particular detail level.
     * Each tile set to be used must be registered using this method,
     * and at least one tile set must be registered for the TileView to render any tiles.
     *
     * @param detailScale (float) scale at which the TileView should use the tiles in this set.
     * @param pattern     (String) string path to the location of the tile image files, to be parsed by a DetailLevelPatternParser
     * @param downsample  (String) string path to the location of an optional non-tiled single image file that will fill the tile view, on a z-layer below tiles
     * @param tileWidth   (int) size of each tiled column
     * @param tileHeight  (int) size of each tiled row
     */
    public void addDetailLevel(float detailScale, String pattern, String downsample, int tileWidth, int tileHeight) {
        detailManager.addDetailLevel(detailScale, pattern, downsample, tileWidth, tileHeight);
    }

    /**
     * Clear all previously registered zoom levels.  This method is experimental.
     */
    public void resetDetailLevels() {
        detailManager.resetDetailLevels();
        refresh();
    }

    /**
     * While the detail level is locked (after this method is invoked, and before unlockDetailLevel is invoked),
     * the DetailLevel will not change, and the current DetailLevel will be scaled beyond the normal
     * bounds.  Normally, during any scale change the details manager searches for the DetailLevel with
     * a registered scale closest to the defined scale.  While locked, this does not occur.
     */
    public void lockDetailLevel() {
        detailManager.lockDetailLevel();
    }

    /**
     * Unlocks a DetailLevel locked with lockDetailLevel
     */
    public void unlockDetailLevel() {
        detailManager.unlockDetailLevel();
    }

    /**
     * pads the viewport by the number of pixels passed.  e.g., setViewportPadding( 100 ) instructs the
     * TileView to interpret it's actual viewport offset by 100 pixels in each direction (top, left,
     * right, bottom), so more tiles will qualify for "visible" status when intersections are calculated.
     *
     * @param padding (int) the number of pixels to pad the viewport by
     */
    public void setViewportPadding(int padding) {
        detailManager.setPadding(padding);
    }

    /**
     * Define a custom parser to manage String file names representing image tiles
     *
     * @param parser (DetailLevelPatternParser) parser that returns String objects from passed pattern, column and row.
     */
    public void setTileSetPatternParser(DetailLevelPatternParser parser) {
        detailManager.setDetailLevelPatternParser(parser);
    }

    //------------------------------------------------------------------------------------
    // Positioning API
    //------------------------------------------------------------------------------------

    /**
     * Register a set of offset points to use when calculating position within the TileView.
     * Any type of coordinate system can be used (any type of lat/lng, percentile-based, etc),
     * and all positioned are calculated relatively.  If relative bounds are defined, position parameters
     * received by TileView methods will be translated to the the appropriate pixel value.
     * To remove this process, use undefineRelativeBounds
     *
     * @param left   (double) the left edge of the rectangle used when calculating position
     * @param top    (double) the top edge of the rectangle used when calculating position
     * @param right  (double) the right edge of the rectangle used when calculating position
     * @param bottom (double) the bottom edge of the rectangle used when calculating position
     */
    public void defineRelativeBounds(double left, double top, double right, double bottom) {
        positionManager.setBounds(left, top, right, bottom);
    }

    /**
     * Unregisters arbitrary bounds and coordinate system.  After invoking this method, TileView methods that
     * receive position method parameters will use pixel values, relative to the TileView's registered size (at 1.0d scale)
     */
    public void undefineRelativeBounds() {
        positionManager.unsetBounds();
    }

    /**
     * Translate a relative x and y position into a Point object with x and y values populated as pixel values, relative to the size of the TileView.
     *
     * @param x (int) relative x position to be translated to absolute pixel value
     * @param y (int) relative y position to be translated to absolute pixel value
     * @return Point a Point object with x and y values calculated from the relative Position x and y values
     */
    public Point translate(double x, double y) {
        return positionManager.translate(x, y);
    }

    /**
     * Translate a List of relative x and y positions (double array... { x, y }
     * into Point objects with x and y values populated as pixel values, relative to the size of the TileView.
     *
     * @param positions (List<double[]>) List of 2-element double arrays to be translated to Points (pixel values).  The first double should represent the relative x value, the second is y
     * @return List<Point> List of Point objects with x and y values calculated from the corresponding x and y values
     */
    public List<Point> translate(List<double[]> positions) {
        return positionManager.translate(positions);
    }

    /**
     * Divides a number by the current scale value, effectively flipping scaled values.  This can be useful when
     * determining a relative position or dimension from a real pixel value.
     *
     * @param value (double) The number to be inversely scaled.
     * @return (double) The inversely scaled product.
     */
    public double unscale(double value) {
        return value / getScale();
    }

    /**
     * Scrolls (instantly) the TileView to the x and y positions provided.
     *
     * @param x (double) the relative x position to move to
     * @param y (double) the relative y position to move to
     */
    public void moveTo(double x, double y) {
        Point point = positionManager.translate(x, y, getScale());
        scrollToPoint(point);
        requestRender();
    }

    /**
     * Scrolls (instantly) the TileView to the x and y positions provided, then centers the viewport to the position.
     *
     * @param x (double) the relative x position to move to
     * @param y (double) the relative y position to move to
     */
    public void moveToAndCenter(double x, double y) {
        Point point = positionManager.translate(x, y, getScale());
        scrollToAndCenter(point);
        requestRender();
    }

    /**
     * Scrolls (with animation) the TIelView to the relative x and y positions provided.
     *
     * @param x (double) the relative x position to move to
     * @param y (double) the relative y position to move to
     */
    public void slideTo(double x, double y) {
        Point point = positionManager.translate(x, y, getScale());
        slideToPoint(point);
    }

    /**
     * Scrolls (with animation) the TileView to the x and y positions provided, then centers the viewport to the position.
     *
     * @param x (double) the relative x position to move to
     * @param y (double) the relative y position to move to
     */
    public void slideToAndCenter(double x, double y) {
        Point point = positionManager.translate(x, y, getScale());
        slideToAndCenter(point);
    }

    /**
     * Scales and moves TileView so that each of the passed points is visible.
     *
     * @param points (List<double[]>) List of 2-element double arrays to be translated to Points (pixel values).  The first double should represent the relative x value, the second is y
     */
    public void framePoints(List<double[]> points) {

        double topMost = -Integer.MAX_VALUE;
        double bottomMost = Integer.MAX_VALUE;
        double leftMost = Integer.MAX_VALUE;
        double rightMost = -Integer.MAX_VALUE;

        for (double[] coordinate : points) {
            double x = coordinate[0];
            double y = coordinate[1];
            if (positionManager.contains(x, y)) {
                topMost = Math.max(topMost, x);
                bottomMost = Math.min(bottomMost, x);
                leftMost = Math.min(leftMost, y);
                rightMost = Math.max(rightMost, y);
            }
        }

        Point topRight = translate(topMost, rightMost);
        Point bottomLeft = translate(bottomMost, leftMost);

        int width = bottomLeft.x - topRight.x;
        int height = bottomLeft.y - topRight.y;

        double scaleX = Math.abs(getWidth() / (double) width);
        double scaleY = Math.abs(getHeight() / (double) height);

        double destinationScale = Math.min(scaleX, scaleY);

        double middleX = (rightMost + leftMost) * 0.5f;
        double middleY = (topMost + bottomMost) * 0.5f;

        moveToAndCenter(middleY, middleX);
        setScaleFromCenter(destinationScale);

    }


    //------------------------------------------------------------------------------------
    // Marker, Callout and HotSpot API
    //------------------------------------------------------------------------------------

    /**
     * Markers added to this TileView will have anchor logic applied on the values provided here.
     * E.g., setMarkerAnchorPoints(-0.5f, -1.0f) will have markers centered horizontally, positioned
     * vertically to a value equal to - 1 * height.
     * Note that individual markers can be assigned specific anchors - this method applies a default
     * value to all markers added without specifying anchor values.
     *
     * @param anchorX (float) the x-axis position of a marker will be offset by a number equal to the width of the marker multiplied by this value
     * @param anchorY (float) the y-axis position of a marker will be offset by a number equal to the height of the marker multiplied by this value
     */
    public void setMarkerAnchorPoints(float anchorX, float anchorY) {
        markerManager.setAnchors(anchorX, anchorY);
    }

    /**
     * Add a marker to the the TileView.  The marker can be any View.
     * No LayoutParams are required; the View will be laid out using WRAP_CONTENT for both width and height, and positioned based on the parameters
     *
     * @param view (View) View instance to be added to the TileView
     * @param x    (double) relative x position the View instance should be positioned at
     * @param y    (double) relative y position the View instance should be positioned at
     * @return (View) the View instance added to the TileView
     */
    public View addMarker(View view, double x, double y) {
        Point point = positionManager.translate(x, y);
        return markerManager.addMarker(view, point.x, point.y);
    }

    /**
     * Add a marker to the the TileView.  The marker can be any View.
     * No LayoutParams are required; the View will be laid out using WRAP_CONTENT for both width and height, and positioned based on the parameters
     *
     * @param view (View) View instance to be added to the TileView
     * @param x    (double) relative x position the View instance should be positioned at
     * @param y    (double) relative y position the View instance should be positioned at
     * @return (View) the View instance added to the TileView
     */
    public View addMarker(View view, double x, double y, float anchorX, float anchorY) {
        Point point = positionManager.translate(x, y);
        return markerManager.addMarker(view, point.x, point.y, anchorX, anchorY);
    }

    /**
     * Removes a marker View from the TileView's view tree.
     *
     * @param view (View) The marker View to be removed.
     */
    public void removeMarker(View view) {
        markerManager.removeMarker(view);
    }

    /**
     * Moves an existing marker to another position.
     *
     * @param view The marker View to be repositioned.
     * @param x    (double) relative x position the View instance should be positioned at
     * @param y    (double) relative y position the View instance should be positioned at
     */
    public void moveMarker(View view, double x, double y) {
        Point point = positionManager.translate(x, y);
        markerManager.moveMarker(view, point.x, point.y);
    }

    /**
     * Moves an existing marker to another position.
     *
     * @param view The marker View to be repositioned.
     * @param x    (double) relative x position the View instance should be positioned at
     * @param y    (double) relative y position the View instance should be positioned at
     */
    public void moveMarker(View view, double x, double y, float anchorX, float anchorY) {
        Point point = positionManager.translate(x, y);
        markerManager.moveMarker(view, point.x, point.y, anchorX, anchorY);
    }

    /**
     * Scroll the TileView so that the View passed is centered in the viewport
     *
     * @param view (View) the View marker that the TileView should center on.
     * @params animate (boolean) should the movement use a transition effectg
     */
    public void moveToMarker(View view, boolean animate) {
        if (markerManager.indexOfChild(view) > -1) {
            LayoutParams params = view.getLayoutParams();
            if (params instanceof AnchorLayout.LayoutParams) {
                AnchorLayout.LayoutParams anchorLayoutParams = (AnchorLayout.LayoutParams) params;
                int scaledX = (int) (anchorLayoutParams.x * getScale());
                int scaledY = (int) (anchorLayoutParams.y * getScale());
                Point point = new Point(scaledX, scaledY);
                if (animate) {
                    slideToAndCenter(point);
                } else {
                    scrollToAndCenter(point);
                }
            }
        }
    }

    /**
     * Scroll the TileView so that the View passed is centered in the viewport
     *
     * @param view (View) the View marker that the TileView should center on.
     */
    public void moveToMarker(View view) {
        moveToMarker(view, false);
    }

    /**
     * Register a MarkerEventListener.  Unlike standard touch events attached to marker View's (e.g., View.OnClickListener),
     * MarkerEventListeners do not consume the touch event, so will not interfere with scrolling.  While the event is
     * dispatched from a Tap event, it's routed though a hit detection API to trigger the listener.
     *
     * @param listener (MarkerEventListener) listener to be added to the TileView's list of MarkerEventListeners
     */
    public void addMarkerEventListener(MarkerEventListener listener) {
        markerManager.addMarkerEventListener(listener);
    }

    /**
     * Removes a MarkerEventListener from the TileView's registry.
     *
     * @param listener (MarkerEventListener) listener to be removed From the TileView's list of MarkerEventListeners
     */
    public void removeMarkerEventListener(MarkerEventListener listener) {
        markerManager.removeMarkerEventListener(listener);
    }

    /**
     * Add a callout to the the TileView.  The callout can be any View.
     * No LayoutParams are required; the View will be laid out using WRAP_CONTENT for both width and height, and positioned based on the parameters
     * Callout views will always be positioned at the top of the view tree (at the highest z-index), and will always be removed during any touch event
     * that is not consumed by the callout View.
     *
     * @param view (View) View instance to be added to the TileView's
     * @param x    (double) relative x position the View instance should be positioned at
     * @param y    (double) relative y position the View instance should be positioned at
     * @return (View) the View instance added to the TileView's
     */
    public View addCallout(View view, double x, double y) {
        Point point = positionManager.translate(x, y);
        return calloutManager.addMarker(view, point.x, point.y);
    }

    /**
     * Add a callout to the the TileView.  The callout can be any View.
     * No LayoutParams are required; the View will be laid out using WRAP_CONTENT for both width and height, and positioned based on the parameters
     * Callout views will always be positioned at the top of the view tree (at the highest z-index), and will always be removed during any touch event
     * that is not consumed by the callout View.
     *
     * @param view (View) View instance to be added to the TileView's
     * @param x    (double) relative x position the View instance should be positioned at
     * @param y    (double) relative y position the View instance should be positioned at
     * @return (View) the View instance added to the TileView's
     */
    public View addCallout(View view, double x, double y, float anchorX, float anchorY) {
        Point point = positionManager.translate(x, y);
        return calloutManager.addMarker(view, point.x, point.y, anchorX, anchorY);
    }

    /**
     * Removes a callout View from the TileView's view tree.
     *
     * @param view The callout View to be removed.
     * @return (boolean) true if the view was in the view tree and was removed, false if it was not in the view tree
     */
    public boolean removeCallout(View view) {
        if (calloutManager.indexOfChild(view) > -1) {
            calloutManager.removeView(view);
            return true;
        }
        return false;
    }

    /**
     * Register a HotSpot that should fire an listener when a touch event occurs that intersects that rectangle.
     * The HotSpot moves and scales with the TileView.
     *
     * @param hotSpot (HotSpot) the hotspot that is tested against touch events that occur on the TileView
     * @return HotSpot the hotspot created with this method
     */
    public HotSpot addHotSpot(HotSpot hotSpot) {
        hotSpotManager.addHotSpot(hotSpot);
        return hotSpot;
    }

    /**
     * Register a HotSpot that should fire an listener when a touch event occurs that intersects that rectangle.
     * The HotSpot moves and scales with the TileView.
     *
     * @param positions (List<double[]>) List of paired doubles { x, y } that represents the points that make up the region.
     * @return HotSpot the hotspot created with this method
     */
    public HotSpot addHotSpot(List<double[]> positions) {
        List<Point> points = positionManager.translate(positions);
        Path path = PathHelper.pathFromPoints(points);
        path.close();
        RectF bounds = new RectF();
        path.computeBounds(bounds, true);
        Rect rect = new Rect();
        bounds.round(rect);
        Region clip = new Region(rect);
        HotSpot hotSpot = new HotSpot();
        hotSpot.setPath(path, clip);
        return addHotSpot(hotSpot);
    }

    /**
     * Register a HotSpot that should fire an listener when a touch event occurs that intersects that rectangle.
     * The HotSpot moves and scales with the TileView.
     *
     * @param positions (List<double[]>) List of paired doubles { x, y } that represents the points that make up the region.
     * @param listener  (HotSpotEventListener) listener to attach to this hotspot, which will be invoked if a Tap event is fired that intersects the hotspot's Region
     * @return HotSpot the hotspot created with this method
     */
    public HotSpot addHotSpot(List<double[]> positions, HotSpotEventListener listener) {
        HotSpot hotSpot = addHotSpot(positions);
        hotSpot.setHotSpotEventListener(listener);
        return hotSpot;
    }

    /**
     * Remove a HotSpot registered with addHotSpot
     *
     * @param hotSpot (HotSpot) the hotspot to remove
     * @return (boolean) true if a hotspot was removed, false if not
     */
    public void removeHotSpot(HotSpot hotSpot) {
        hotSpotManager.removeHotSpot(hotSpot);
    }

    /**
     * Register a HotSpotEventListener with the TileView.  This listener will fire if any hotspot's region intersects a Tap event.
     *
     * @param listener (HotSpotEventListener) the listener to be added.
     */
    public void addHotSpotEventListener(HotSpotEventListener listener) {
        hotSpotManager.addHotSpotEventListener(listener);
    }

    /**
     * Remove a HotSpotEventListener from the TileView's registry.
     *
     * @param listener (HotSpotEventListener) the listener to be removed
     */
    public void removeHotSpotEventListener(HotSpotEventListener listener) {
        hotSpotManager.removeHotSpotEventListener(listener);
    }

    //------------------------------------------------------------------------------------
    // Path Drawing API
    //------------------------------------------------------------------------------------

    /**
     * Register a Path and Paint that will be drawn on a layer above the tiles, but below markers.
     * This Path's will be scaled with the TileView, but will always be as wide as the stroke set for the Paint.
     *
     * @param drawablePath (DrawablePath) a DrawablePath instance to be drawn by the TileView
     * @return DrawablePath the DrawablePath instance passed to the TileView
     */
    public DrawablePath drawPath(DrawablePath drawablePath) {
        return pathManager.addPath(drawablePath);
    }

    /**
     * Register a Path and Paint that will be drawn on a layer above the tiles, but below markers.
     * This Path's will be scaled with the TileView, but will always be as wide as the stroke set for the Paint.
     *
     * @param positions (List<double[]>) List of doubles { x, y } that represent the points of the Path.
     * @return DrawablePath the DrawablePath instance passed to the TileView
     */
    public DrawablePath drawPath(List<double[]> positions) {
        List<Point> points = positionManager.translate(positions);
        return pathManager.addPath(points);
    }

    /**
     * Register a Path and Paint that will be drawn on a layer above the tiles, but below markers.
     * This Path's will be scaled with the TileView, but will always be as wide as the stroke set for the Paint.
     *
     * @param positions (List<double[]>) List of doubles { x, y } that represent the points of the Path.
     * @param paint     (Paint) the Paint instance that defines the style of the drawn path.
     * @return DrawablePath the DrawablePath instance passed to the TileView
     */
    public DrawablePath drawPath(List<double[]> positions, Paint paint) {
        List<Point> points = positionManager.translate(positions);
        return pathManager.addPath(points, paint);
    }

    /**
     * Removes a DrawablePath from the TileView's registry.  This path will no longer be drawn by the TileView.
     *
     * @param drawablePath (DrawablePath) the DrawablePath instance to be removed.
     */
    public void removePath(DrawablePath drawablePath) {
        pathManager.removePath(drawablePath);
    }

    /**
     * Returns the Paint instance used by default.  This can be modified for future Path paint operations.
     *
     * @return Paint the Paint instance used by default.
     */
    public Paint getPathPaint() {
        return pathManager.getPaint();
    }

    //------------------------------------------------------------------------------------
    // Memory Management API
    //------------------------------------------------------------------------------------

    /**
     * Clear bitmap image files, appropriate for Activity.onPause
     */
    public void clear() {
        tileManager.clear();
        sampleManager.clear();
        pathManager.setShouldDraw(false);
    }

    /**
     * Clear bitmap image files, appropriate for Activity.onPause (mirror for .clear)
     */
    public void pause() {
        clear();
    }

    /**
     * Clear tile image files and remove all views, appropriate for Activity.onDestroy
     * References to TileView should be set to null following invocations of this method.
     */
    public void destroy() {
        tileManager.clear();
        sampleManager.clear();
        pathManager.clear();
    }

    /**
     * Restore visible state (generally after a call to .clear()
     * Appropriate for Activity.onResume
     */
    public void resume() {
        updateViewport();
        tileManager.requestRender();
        sampleManager.update();
        pathManager.setShouldDraw(true);
    }

    /**
     * Request the TileView reevaluate tile sets, rendered tiles, samples, invalidates, etc
     */
    public void refresh() {
        updateViewport();
        tileManager.updateTileSet();
        tileManager.requestRender();
        sampleManager.update();
        redraw();
    }

    //------------------------------------------------------------------------------------
    // PRIVATE API
    //------------------------------------------------------------------------------------


    // make sure we keep the viewport UTD, and if layout changes we'll need to recompute what tiles to show
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        updateViewport();
        requestRender();
    }

    // let the zoom manager know what tiles to show based on our position and dimensions
    private void updateViewport() {
        int left = getScrollX();
        int top = getScrollY();
        int right = left + getWidth();
        int bottom = top + getHeight();
        detailManager.updateViewport(left, top, right, bottom);
    }

    // tell the tile renderer to not start any more tasks, but it can continue with any that are already running
    private void suppressRender() {
        tileManager.suppressRender();
    }


    //------------------------------------------------------------------------------------
    // Private Listeners
    //------------------------------------------------------------------------------------

    private ZoomPanListener zoomPanListener = new ZoomPanListener() {
        @Override
        public void onZoomPanEvent() {

        }

        @Override
        public void onScrollChanged(int x, int y) {
            updateViewport();
            for (TileViewEventListener listener : tileViewEventListeners) {
                listener.onScrollChanged(x, y);
            }
        }

        @Override
        public void onScaleChanged(double scale) {
            detailManager.setScale(scale);
            for (TileViewEventListener listener : tileViewEventListeners) {
                listener.onScaleChanged(scale);
            }
        }

        @Override
        public void onZoomStart(double scale) {
            detailManager.lockDetailLevel();
            detailManager.setScale(scale);
            for (TileViewEventListener listener : tileViewEventListeners) {
                listener.onZoomStart(scale);
            }
        }

        @Override
        public void onZoomComplete(double scale) {
            detailManager.unlockDetailLevel();
            detailManager.setScale(scale);
            requestRender();  // put this here instead of gesture listener so we catch animations and pinches
            for (TileViewEventListener listener : tileViewEventListeners) {
                listener.onZoomComplete(scale);
            }
        }
    };

    private DetailLevelEventListener detailLevelEventListener = new DetailLevelEventListener() {
        @Override
        public void onDetailLevelChanged() {
            requestRender();
            for (TileViewEventListener listener : tileViewEventListeners) {
                listener.onDetailLevelChanged();
            }
        }

        /*
         * do *not* update scale in response to changes in the zoom manager
         * transactions are one-way - set scale on TileView (ZoomPanLayout)
         * and pass those to DetailManager, which then distributes, manages
         * and notifies all other interested parties.
         */
        @Override
        public void onDetailScaleChanged(double scale) {

        }
    };

    private GestureListener gestureListener = new GestureListener() {

        @Override
        public void onDoubleTap(Point point) {
            for (TileViewEventListener listener : tileViewEventListeners) {
                listener.onDoubleTap(point.x, point.y);
            }
        }

        @Override
        public void onDrag(Point point) {
            suppressRender();
            for (TileViewEventListener listener : tileViewEventListeners) {
                listener.onDrag(point.x, point.y);
            }
        }

        @Override
        public void onFingerDown(Point point) {
            suppressRender();
            for (TileViewEventListener listener : tileViewEventListeners) {
                listener.onFingerDown(point.x, point.y);
            }
        }

        @Override
        public void onFingerUp(Point point) {
            if (!isFlinging()) {
                requestRender();
            }
            for (TileViewEventListener listener : tileViewEventListeners) {
                listener.onFingerUp(point.x, point.y);
            }
        }

        @Override
        public void onFling(Point startPoint, Point finalPoint) {
            suppressRender();
            for (TileViewEventListener listener : tileViewEventListeners) {
                listener.onFling(startPoint.x, startPoint.y, finalPoint.x, finalPoint.y);
            }
        }

        @Override
        public void onFlingComplete(Point point) {
            requestRender();
            for (TileViewEventListener listener : tileViewEventListeners) {
                listener.onFlingComplete(point.x, point.y);
            }
        }

        @Override
        public void onPinch(Point point) {
            suppressRender();
            for (TileViewEventListener listener : tileViewEventListeners) {
                listener.onPinch(point.x, point.y);
            }
        }

        @Override
        public void onPinchComplete(Point point) {
            requestRender();
            for (TileViewEventListener listener : tileViewEventListeners) {
                listener.onPinchComplete(point.x, point.y);
            }
        }

        @Override
        public void onPinchStart(Point point) {
            suppressRender();
            for (TileViewEventListener listener : tileViewEventListeners) {
                listener.onPinchStart(point.x, point.y);
            }
        }

        @Override
        public void onTap(Point point) {
            markerManager.processHit(point);
            hotSpotManager.processHit(point);
            for (TileViewEventListener listener : tileViewEventListeners) {
                listener.onTap(point.x, point.y);
            }
        }

        @Override
        public void onScrollComplete(Point point) {
            requestRender();
            for (TileViewEventListener listener : tileViewEventListeners) {
                listener.onScrollChanged(point.x, point.y);
            }
        }
    };

    private TileRenderListener renderListener = new TileRenderListener() {
        @Override
        public void onRenderCancelled() {

        }

        @Override
        public void onRenderComplete() {
            for (TileViewEventListener listener : tileViewEventListeners) {
                listener.onRenderComplete();
            }
        }

        @Override
        public void onRenderStart() {
            for (TileViewEventListener listener : tileViewEventListeners) {
                listener.onRenderStart();
            }
        }
    };

    //------------------------------------------------------------------------------------
    // Public static interfaces and classes
    //------------------------------------------------------------------------------------

    /**
     * Interface for implementations to receive TileView events.  This interface consolidates several disparate
     * listeners (Gestures, ZoomPan Events, TileView events) into a single unit for ease of use.
     */
    public static interface TileViewEventListener {
        /**
         * Fires when a ACTION_DOWN event is raised from the TileView
         *
         * @param x (int) the x position of the event
         * @param y (int) the y position of the event
         */
        public void onFingerDown(int x, int y);

        /**
         * Fires when a ACTION_UP event is raised from the TileView
         *
         * @param x (int) the x position of the event
         * @param y (int) the y position of the event
         */
        public void onFingerUp(int x, int y);

        /**
         * Fires while the TileView is being dragged
         *
         * @param x (int) the x position of the event
         * @param y (int) the y position of the event
         */
        public void onDrag(int x, int y);

        /**
         * Fires when a user double-taps the TileView
         *
         * @param x (int) the x position of the event
         * @param y (int) the y position of the event
         */
        public void onDoubleTap(int x, int y);

        /**
         * Fires when a user taps the TileView
         *
         * @param x (int) the x position of the event
         * @param y (int) the y position of the event
         */
        public void onTap(int x, int y);

        /**
         * Fires while a user is pinching the TileView
         *
         * @param x (int) the x position of the event
         * @param y (int) the y position of the event
         */
        public void onPinch(int x, int y);

        /**
         * Fires when a user starts a pinch action
         *
         * @param x (int) the x position of the event
         * @param y (int) the y position of the event
         */
        public void onPinchStart(int x, int y);

        /**
         * Fires when a user completes a pinch action
         *
         * @param x (int) the x position of the event
         * @param y (int) the y position of the event
         */
        public void onPinchComplete(int x, int y);

        /**
         * Fires when a user initiates a fling action
         *
         * @param sx (int) the x position of the start of the fling
         * @param sy (int) the y position of the start of the fling
         * @param dx (int) the x position of the end of the fling
         * @param dy (int) the y position of the end of the fling
         */
        public void onFling(int sx, int sy, int dx, int dy);

        /**
         * Fires when a fling action has completed
         *
         * @param x (int) the final x scroll position of the TileView after the fling
         * @param y (int) the final y scroll position of the TileView after the fling
         */
        public void onFlingComplete(int x, int y);

        /**
         * Fires when the TileView's scale has updated
         *
         * @param scale (double) the new scale of the TileView (0-1)
         */
        public void onScaleChanged(double scale);

        /**
         * Fires when the TileView's scroll position has updated
         *
         * @param x (int) the new x scroll position of the TileView
         * @param y (int) the new y scroll position of the TileView
         */
        public void onScrollChanged(int x, int y);

        /**
         * Fires when a zoom action starts (typically through a pinch of double-tap action,
         * or by programmatic animated zoom methods.
         *
         * @param scale (double) the new scale of the TileView (0-1)
         */
        public void onZoomStart(double scale);

        /**
         * Fires when a zoom action ends (typically through a pinch of double-tap action,
         * or by programmatic animated zoom methods.
         *
         * @param scale (double) the new scale of the TileView (0-1)
         */
        public void onZoomComplete(double scale);

        /**
         * Fires when the TileView should start using a new DetailLevel
         */
        public void onDetailLevelChanged();

        /**
         * Fires when the rendering thread has started to update the visible tiles.
         */
        public void onRenderStart();

        /**
         * Fires when the rendering thread has completed updating the visible tiles, but before cleanup
         */
        public void onRenderComplete();
    }

    /**
     * Convenience class that implements {@TileViewEventListener}
     */
    public static class TileViewEventListenerImplementation implements TileViewEventListener {

        /**
         * Fires when a ACTION_DOWN event is raised from the TileView
         *
         * @param x (int) the x position of the event
         * @param y (int) the y position of the event
         */
        public void onFingerDown(int x, int y) {

        }

        /**
         * Fires when a ACTION_UP event is raised from the TileView
         *
         * @param x (int) the x position of the event
         * @param y (int) the y position of the event
         */
        public void onFingerUp(int x, int y) {

        }

        /**
         * Fires while the TileView is being dragged
         *
         * @param x (int) the x position of the event
         * @param y (int) the y position of the event
         */
        public void onDrag(int x, int y) {

        }

        /**
         * Fires when a user double-taps the TileView
         *
         * @param x (int) the x position of the event
         * @param y (int) the y position of the event
         */
        public void onDoubleTap(int x, int y) {

        }

        /**
         * Fires when a user taps the TileView
         *
         * @param x (int) the x position of the event
         * @param y (int) the y position of the event
         */
        public void onTap(int x, int y) {

        }

        /**
         * Fires while a user is pinching the TileView
         *
         * @param x (int) the x position of the event
         * @param y (int) the y position of the event
         */
        public void onPinch(int x, int y) {

        }

        /**
         * Fires when a user starts a pinch action
         *
         * @param x (int) the x position of the event
         * @param y (int) the y position of the event
         */
        public void onPinchStart(int x, int y) {

        }

        /**
         * Fires when a user completes a pinch action
         *
         * @param x (int) the x position of the event
         * @param y (int) the y position of the event
         */
        public void onPinchComplete(int x, int y) {

        }

        /**
         * Fires when a user initiates a fling action
         *
         * @param sx (int) the x position of the start of the fling
         * @param sy (int) the y position of the start of the fling
         * @param dx (int) the x position of the end of the fling
         * @param dy (int) the y position of the end of the fling
         */
        public void onFling(int sx, int sy, int dx, int dy) {

        }

        /**
         * Fires when a fling action has completed
         *
         * @param x (int) the final x scroll position of the TileView after the fling
         * @param y (int) the final y scroll position of the TileView after the fling
         */
        public void onFlingComplete(int x, int y) {

        }

        /**
         * Fires when the TileView's scale has updated
         *
         * @param scale (double) the new scale of the TileView (0-1)
         */
        public void onScaleChanged(double scale) {

        }

        /**
         * Fires when the TileView's scroll position has updated
         *
         * @param x (int) the new x scroll position of the TileView
         * @param y (int) the new y scroll position of the TileView
         */
        public void onScrollChanged(int x, int y) {

        }

        /**
         * Fires when a zoom action starts (typically through a pinch of double-tap action,
         * or by programmatic animated zoom methods.
         *
         * @param scale (double) the new scale of the TileView (0-1)
         */
        public void onZoomStart(double scale) {

        }

        /**
         * Fires when a zoom action ends (typically through a pinch of double-tap action,
         * or by programmatic animated zoom methods.
         *
         * @param scale (double) the new scale of the TileView (0-1)
         */
        public void onZoomComplete(double scale) {

        }

        /**
         * Fires when the TileView should start using a new DetailLevel
         *
         */
        public void onDetailLevelChanged() {

        }

        /**
         * Fires when the rendering thread has started to update the visible tiles.
         */
        public void onRenderStart() {

        }

        /**
         * Fires when the rendering thread has completed updating the visible tiles, but before cleanup
         */
        public void onRenderComplete() {

        }

    }

    public PositionManager getPositionManager() {
        return positionManager;
    }

    public PathManager getPathManager() {
        return pathManager;
    }

}